package edu.tamu.cse.teamproject1;

import java.security.InvalidParameterException;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Stack;

import edu.tamu.cse.teamproject1.Database.Exceptions.InvalidColumnException;
import edu.tamu.cse.teamproject1.Database.Exceptions.NullReferenceException;
import edu.tamu.cse.teamproject1.Database.Exceptions.SqlException;
import edu.tamu.cse.teamproject1.Database.Interfaces.IColumnDefinition;
import edu.tamu.cse.teamproject1.Database.Interfaces.IDatabaseTable;
import edu.tamu.cse.teamproject1.Database.Interfaces.ITuple;
import edu.tamu.cse.teamproject1.Database.Interfaces.ITupleElement;

/**
 * Filters result sets based on a WHERE clause
 * 
 * @author Christopher Weldon <chris@chrisweldon.net>
 * @since Release 0.1.0
 */
public class QueryFilter {
	
	/**
	 * Database table the filter should be applied to
	 */
	private IDatabaseTable _table;
	
	/**
	 * Query that helps filter the results
	 */
	private String _filter;
	
	/**
	 * Checks to see if the operator passed is a valid operator or not
	 * 
	 * @param operator
	 * @return Is the string an operator?
	 */
	protected boolean checkOperator(String operator) {
		String temp = operator.toUpperCase();
		if (temp.equals("IN") || temp.equals("EXISTS") || temp.equals("AND") || temp.equals("OR") ||
			temp.equals("NOT") || temp.equals("ALL") || temp.equals("ANY")) {
			return true;
		} else {
			return false;
		}
	}
	
	/**
	 * Removes all non-value spaces from the query string
	 * 
	 * @param query
	 * @return Query without spaces
	 * @todo Fix to handle IN, EXISTS, ANY, and ALL
	 */
	protected String trimSpaces(String query) {
		String trimmedQuery = "";
		boolean inValue = false;
		String[] tokens = query.split(" ");
		for (String token : tokens) {
			if (checkOperator(token)) {
				trimmedQuery += " " + token + " ";
			} else if (token.contains("'")) {
				// If the token is strictly a value, then add and continue
				if (token.startsWith("'") && token.endsWith("'")) {
					trimmedQuery += token;
				} else {
					if (inValue) {
						trimmedQuery += " " + token;
					} else {
						trimmedQuery += token;
					}
					inValue = !inValue;
				}
			} else {
				if (inValue) {
					trimmedQuery += " ";
				}
				trimmedQuery += token;
			}
		}
		
		return trimmedQuery;
	}
	
	/**
	 * Parses the filter query using a couple of different stacks
	 * 
	 * This is probably the most complex method in this class. It will break apart
	 * the query character by character and recreate the sub-queries to as to be
	 * able to stack the requests in order of precedence. 
	 * 
	 * We convert the query into one without spaces (except for values) so that
	 * we don't have to deal with spaces. 
	 * 
	 * @param query
	 * @return Stack containing order of operations
	 */
	protected Stack<String> parseFilterQuery(String query) {
		// Get rid of non-essential spaces
		query = this.trimSpaces(query);
		
		// Setup the values we'll need during parsing
		Stack<String> tempOperationStack = new Stack<String>();
		Stack<String> operationStack = new Stack<String>();
		String tempColName = "";
		String tempColValue = "";
		String tempOperator = "";
		boolean inValue = false, inQuoted = false;
		
		// Proceed with parsing character by character
		for (char c : query.toCharArray()) {
			if (c == ' ' && !inValue) {
				// We've entered a space before a non-standard operator (e.g. ANY, ALL, etc.). 
				// Clear the buffers by adding them to the stack and continue
				if (tempColName != "") {
					tempOperationStack.push(tempColName);
					tempColName = "";
				}
				if (tempOperator != "") {
					tempOperationStack.push(tempOperator);
					tempOperator = "";
				}
				if (tempColValue != "") {
					tempOperationStack.push(tempColValue);
					tempColValue = "";
				}
			} else if (c == '(' && !inValue && !inQuoted) {
				// This is the beginning of a clause. We take everything that's on the tempColName,
				// tempOperator, and tempColValue and push down onto the stack, in that order. This 
				// helps to clear the state, as there should not be any lingering values in those buffers
				
				// Check if the name buffer is empty
				if (tempColName != "") {
					tempOperationStack.push(tempColName);
					tempColName = "";
				}
				
				// Check if the operator buffer is empty
				if (tempOperator != "") {
					tempOperationStack.push(tempOperator);
					tempOperator = "";
				}
				
				// Push the opening brace onto the stack for later consumption. Mark that we're not in a value
				tempOperationStack.push("(");
				inValue = false;
			} else if (c == ')' && !inQuoted) {
				// This is the ending of a clause. We take everything from the temporary stack, pop it off
				// and push it down onto the new stack. This follows from the PREFIX notation learned in
				// CPSC221. Before popping off the stack, we have to check to make sure there are no outstanding
				// values to push onto the stack.
				
				// Need to first push the value down onto the stack and clear its temp storage
				if (tempColValue != "") {
					tempOperationStack.push(tempColValue);
					tempColValue = "";
				}
				
				// Pop top 3 elements (name, operator, and value) off the temp stack, assemble them, 
				// and push onto the new stack
				String tempVal = "";
				while (tempOperationStack.peek() != "(") {
					// Assemble in reverse
					tempVal = tempOperationStack.pop() + tempVal;
				}
				operationStack.push(tempVal);
				// Pop the "(" back off the stack
				tempOperationStack.pop();
				inValue = false;
			} else if (inValue && c != '\'') {
				// Take all characters as long as we're in value
				tempColValue += c;
			} else if (c == '\'') {
				// In the event that we run across a single quote, which is an indicator of values,
				// then we need to flip some flags around to make the process state-aware. Before
				// updating the state, we need to get the name and operator values onto the stack. 
				if (tempColName != "") {
					tempOperationStack.push(tempColName);
					tempColName = "";
				}
				if (tempOperator != "") {
					tempOperationStack.push(tempOperator);
					tempOperator = "";
				}
				
				// Update the state and save the value
				inValue = !inValue;
				inQuoted = !inQuoted;
				tempColValue += c;
			} else if (c == '!' || c == '=' || c == '>' || c == '<') {
				// We're checking for basic operators. By this point, if one of
				// the previous cases did not match, then we're not inValue, meaning
				// it's safe to try to match the operators. Before storing the operator,
				// push names down onto the stack. 
				if (tempColName != "") {
					tempOperationStack.push(tempColName);
					tempColName = "";
				}
				// We're collecting an operator
				tempOperator += c;
			} else {
				// Finally, we assume that we're gathering column names. Check if the operator
				// is empty or not, and push down onto the stack if it isn't empty. 
				if (tempOperator != "") {
					// Push the operator onto the stack and clear it
					tempOperationStack.push(tempOperator);
					tempOperator = "";
					inValue = true;
					tempColValue += c;
				} else {
					// No need to freak - we're just collecting a column name
					tempColName += c;
					// We actually collected an operator if this is true. Push it
					// onto the stack.
					if (checkOperator(tempColName)) {
						tempOperationStack.push(tempColName);
						tempColName = "";
						inValue = true;
					}
				}
			}
		}
			
		// Check for missing values and push them onto the temp stack. 
		if (tempColValue != "") {
			tempOperationStack.push(tempColValue);
			tempColValue = "";
		}
		
		// Get everything off the temp stack (that's not a parenthesis) and push down onto the main stack
		String tempVal = "";
		while (tempOperationStack.size() > 0) {
			String tempPop = tempOperationStack.pop();
			if (tempPop != "(" && tempPop != ")") {
				tempVal = tempPop + tempVal;
			}
		}
		if (tempVal != "") {
			operationStack.push(tempVal);
		}
		return operationStack;
	}
	
	/**
	 * Retrieves a list of matching elements from a list of elements
	 * 
	 * This method takes several arguments (the first of which being a list of
	 * Tuples to filter) and will apply a comparator to a column and return
	 * only the matching tuples.
	 * 
	 * If a columnName is being searched that doesn't exist in the table, then
	 * an InvalidColumnException is thrown.
	 * 
	 * @param searchRows Records to filter
	 * @param columnName Name of the column to apply a filter
	 * @param columnValue Value of the column to check
	 * @param comparator How the values should be evaluated? (<, <=, >, =, etc.)
	 * @return List of matching elements
	 * @throws InvalidParameterException, InvalidColumnException, NullReferenceException
	 */
	protected List<ITuple> getMatchingRows(List<ITuple> searchRows, String columnName, 
			Object columnValue, String comparator) 
			throws InvalidParameterException, InvalidColumnException, NullReferenceException {
		// Do some sanity checking
		if (searchRows == null || searchRows.size() == 0) {
			// Return an empty array list if we have nothing to search.
			return new ArrayList<ITuple>();
		}
		if (columnName == null || columnName.equals("")) {
			throw new InvalidParameterException("columnName must not be null or empty.");
		}
		if (columnValue == null || columnValue.toString().equals("")) {
			throw new InvalidParameterException("columnValue must not be null or empty.");
		}
		if (comparator == null || comparator.equals("")) {
			throw new InvalidParameterException("comparator must not be null or empty.");
		}
		
		// Quick check to see if the columnName exists. Throw an exception if not.
		if (!this.checkColumnNameExists(columnName)) {
			throw new InvalidColumnException("Column name " + columnName + " does not exist.");
		}
		
		// Start searching for tuples.
		ArrayList<ITuple> tuples = new ArrayList<ITuple>();
		for (ITuple tuple : searchRows) {
			List<ITupleElement> tupleElements = tuple.getAllElements();
			for (ITupleElement element : tupleElements) {
				if (element.getColumnName().equals(columnName)) {
					// TODO: Cast to the appropriate type and compare values
					switch (element.getColumnType()) {
					case character:
					case varchar:
						if (ComparatorParser.compare(element.getValue().toString(), columnValue.toString(), comparator)) {
							tuples.add(tuple);
						}
						break;
					case integer:
						if (ComparatorParser.compare(Integer.parseInt(element.getValue().toString()), Integer.parseInt(columnValue.toString()), comparator)) {
							tuples.add(tuple);
						}
						break;
					case floating:
						if (ComparatorParser.compare(Float.parseFloat(element.getValue().toString()), Float.parseFloat(columnValue.toString()), comparator)) {
							tuples.add(tuple);
						}
						break;
					case date:
						Date elemDate = (Date)element.getValue();
						Date colValue = (Date)columnValue;
						if (ComparatorParser.compare(elemDate, colValue, comparator)) {
							tuples.add(tuple);
						}
						break;
					default:
						// TODO: Figure out how to handle this situation
						break;
					}
				}
			}
		}
		
		return tuples;
	}
	
	/**
	 * Checks if the column name exists in the table
	 * 
	 * @param columnName Name of column to check for
	 * @return true if column name exists
	 * @throws NullReferenceException if database table is not set
	 */
	protected boolean checkColumnNameExists(String columnName) 
		throws NullReferenceException {
		// Sanity check
		if (this._table == null) {
			throw new NullReferenceException("Database table not set.");
		}
		
		// Iterate over the column definitions and try to find the column
		boolean columnNameExists = false;
		for (IColumnDefinition column : this._table.getColumnLists()) {
			if (column.getColumnName().equals(columnName)) {
				columnNameExists = true;
				break;
			}
		}
		return columnNameExists;
	}
	
	/**
	 * Takes the query stack and performs the query
	 * 
	 * This method will make use of two stacks, one to place waiting filters queries
	 * (when trying to find the operator such as AND, OR, and NOT). The other stack
	 * is the original input stack.
	 * 
	 * @param queryStack
	 * @return List of results from the query
	 * @throws SqlException
	 * @todo Test this method
	 */
	protected List<ITuple> performQuery(Stack<String> queryStack) throws SqlException {
		Stack<String> tempStack = new Stack<String>();
		Stack<List<ITuple>> resultStack = new Stack<List<ITuple>>();
		try {
			while (queryStack.size() > 0) {
				tempStack.push(queryStack.pop());
				if (this.checkOperator(tempStack.peek())) {
					// If we just pushed down an operator, then process the results on the stack
					String operator = tempStack.pop();
					// Pull everything off the temp stack and process, 
					// storing the results in the result stack
					while (tempStack.size() > 0) {
						String[] operation = this.splitKeyValuePair(tempStack.pop());
						List<ITuple> tempResult = this.getMatchingRows(this._table.getAllTuples(), 
								operation[0], operation[1], operation[2]);
						resultStack.push(tempResult);
					}
					// Pop the top 2 results off and perform the operation on them
					List<ITuple> result1 = resultStack.pop();
					List<ITuple> result2 = resultStack.pop();
					resultStack.push(this.mergeResults(result1, result2, operator));
				} else {
					while (tempStack.size() > 0) {
						String[] operation = this.splitKeyValuePair(tempStack.pop());
						List<ITuple> tempResult = this.getMatchingRows(this._table.getAllTuples(), 
								operation[0], operation[1], operation[2]);
						resultStack.push(tempResult);
					}
				}
			}
		} catch (InvalidColumnException e) { 
			throw new SqlException("A column requested has an invalid name.");
		} catch (NullReferenceException e) {
			throw new SqlException("Unable to perform the query.", e);
		}
		
		// The value to return should be the top of the result stack
		return resultStack.pop();
	}
	
	/**
	 * Merges the results of the tuple lists together, removing duplicates
	 * 
	 * @param result1 Result set 1
	 * @param result2 Result set 2
	 * @param operator The merging operation
	 * @return List of merged tuple lists
	 * @todo Test this method
	 */
	protected List<ITuple> mergeResults(List<ITuple> result1, List<ITuple> result2, String operator) {
		List<ITuple> mergedResults = new ArrayList<ITuple>();
		if (operator.equals("OR")) {
			// Add all from tuple list 1 and pick up the missing from result2
			mergedResults.addAll(result1);
			for (ITuple tuple : result2) {
				if (!mergedResults.contains(tuple)) {
					mergedResults.add(tuple);
				}
			} 
		} else if (operator.equals("AND")) {
			// Add all tuples common to both result sets
			for (ITuple tuple : result1) {
				if (result2.contains(tuple)) {
					mergedResults.add(tuple);
				}
			}
		} else if (operator.equals("NOT")) {
			// Add tuples from result1 that are not in result 2
			for (ITuple tuple : result1) {
				if (!result2.contains(tuple)) {
					mergedResults.add(tuple);
				}
			}
		}
		
		return mergedResults;
	}
	
	/**
	 * Takes a key-value pair and splits into an array
	 * 
	 * The resulting array should have the following elements:
	 * 
	 *  0 = key
	 *  1 = value
	 *  2 = operator
	 * 
	 * @param query
	 * @return Array of key value pairs (should be 3 elements)
	 * @todo Fix this method to account for ANY, ALL, IN, or EXISTS
	 */
	protected String[] splitKeyValuePair(String kvpair) {
		String[] exploded = kvpair.split("[!=<>]+");
		String[] newExploded = new String[exploded.length+1];
		newExploded[0] = exploded[0];
		newExploded[1] = exploded[1];
		String temp = kvpair.replaceFirst(exploded[0], "");
		temp = temp.replaceFirst(exploded[1], "");
		newExploded[2] = temp;
		return newExploded;
	}
	
	/**
	 * Constructs a query filter object
	 */
	public QueryFilter() {}
	
	/**
	 * Constructor accepting a Database Table as an argument
	 * 
	 * @param IDatabaseTable table
	 */
	public QueryFilter(IDatabaseTable table) {
		this._table = table;
	}
	
	/**
	 * Applies a filter locally
	 * 
	 * The application doesn't do anything initially. It's application becomes
	 * apparent later when the consumer of this object retrieves the list of
	 * results. 
	 * 
	 * @param String queryFilter
	 */
	public void applyFilter(String queryFilter) {
		this._filter = queryFilter;
	}
	
	/**
	 * Sets the database table that will be used for consuming results
	 * 
	 * @param table
	 */
	public void setTable(IDatabaseTable table) {
		this._table = table;
	}
	
	/**
	 * Retrieves a list of results
	 * 
	 * This method will fetch a list of rows from the databaseTable based on the
	 * criterion specified in the filter. 
	 * 
	 * @return List of rows matching the query filter
	 * @throws NullReferenceException, SqlException
	 */
	public List<ITuple> fetchResults() throws NullReferenceException, SqlException {
		if (this._table == null) {
			throw new NullReferenceException("No table to operate on.");
		}
		
		// Return all the tuples if there's no filter to be applied
		if (this._filter == null || this._filter.equals("")) {
			return this._table.getAllTuples();
		} else {
			// Parse the query and make the comparison on the operators
			Stack<String> resultComparators = this.parseFilterQuery(this._filter);
			try {
				return this.performQuery(resultComparators);
			} catch (SqlException e) {
				throw e;
			}
		}
	}
}	